Jelajahi tipe tepat TypeScript untuk pencocokan bentuk objek yang ketat, mencegah properti tak terduga dan memastikan ketangguhan kode. Pelajari aplikasi praktis dan praktik terbaik.
Tipe Tepat TypeScript: Pencocokan Bentuk Objek yang Ketat untuk Kode yang Tangguh
TypeScript, sebuah superset dari JavaScript, membawa pengetikan statis ke dunia pengembangan web yang dinamis. Meskipun TypeScript menawarkan keuntungan signifikan dalam hal keamanan tipe dan kemudahan pemeliharaan kode, sistem pengetikan strukturalnya terkadang dapat menyebabkan perilaku yang tidak terduga. Di sinilah konsep "tipe tepat" berperan. Meskipun TypeScript tidak memiliki fitur bawaan yang secara eksplisit bernama "tipe tepat", kita dapat mencapai perilaku serupa melalui kombinasi fitur dan teknik TypeScript. Postingan blog ini akan membahas cara menerapkan pencocokan bentuk objek yang lebih ketat di TypeScript untuk meningkatkan ketangguhan kode dan mencegah kesalahan umum.
Memahami Pengetikan Struktural TypeScript
TypeScript menggunakan pengetikan struktural (juga dikenal sebagai *duck typing*), yang berarti kompatibilitas tipe ditentukan oleh anggota dari tipe tersebut, bukan oleh nama yang dideklarasikan. Jika sebuah objek memiliki semua properti yang dibutuhkan oleh sebuah tipe, maka objek tersebut dianggap kompatibel dengan tipe itu, terlepas dari apakah ia memiliki properti tambahan.
Sebagai contoh:
interface Point {
x: number;
y: number;
}
const myPoint = { x: 10, y: 20, z: 30 };
function printPoint(point: Point) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
printPoint(myPoint); // Ini berfungsi baik, meskipun myPoint memiliki properti 'z'
Dalam skenario ini, TypeScript mengizinkan `myPoint` untuk diteruskan ke `printPoint` karena ia mengandung properti `x` dan `y` yang diperlukan, meskipun memiliki properti `z` tambahan. Meskipun fleksibilitas ini bisa nyaman, hal ini juga dapat menyebabkan bug halus jika Anda secara tidak sengaja meneruskan objek dengan properti yang tidak terduga.
Masalah dengan Properti Berlebih
Kelonggaran pengetikan struktural terkadang dapat menutupi kesalahan. Pertimbangkan sebuah fungsi yang mengharapkan objek konfigurasi:
interface Config {
apiUrl: string;
timeout: number;
}
function setup(config: Config) {
console.log(`API URL: ${config.apiUrl}`);
console.log(`Timeout: ${config.timeout}`);
}
const myConfig = { apiUrl: "https://api.example.com", timeout: 5000, typo: true };
setup(myConfig); // TypeScript tidak komplain di sini!
console.log(myConfig.typo); //mencetak true. Properti tambahan ada secara diam-diam
Dalam contoh ini, `myConfig` memiliki properti tambahan `typo`. TypeScript tidak menimbulkan kesalahan karena `myConfig` masih memenuhi antarmuka `Config`. Namun, kesalahan ketik tersebut tidak pernah terdeteksi, dan aplikasi mungkin tidak berperilaku seperti yang diharapkan jika kesalahan ketik tersebut seharusnya `typoo`. Masalah yang tampaknya tidak signifikan ini dapat berkembang menjadi masalah besar saat men-debug aplikasi yang kompleks. Properti yang hilang atau salah eja bisa sangat sulit dideteksi saat berhadapan dengan objek yang bersarang di dalam objek lain.
Pendekatan untuk Menegakkan Tipe Tepat di TypeScript
Meskipun "tipe tepat" yang sesungguhnya tidak tersedia secara langsung di TypeScript, berikut adalah beberapa teknik untuk mencapai hasil serupa dan menerapkan pencocokan bentuk objek yang lebih ketat:
1. Menggunakan Penegasan Tipe dengan `Omit`
Tipe utilitas `Omit` memungkinkan Anda membuat tipe baru dengan mengecualikan properti tertentu dari tipe yang sudah ada. Dikombinasikan dengan penegasan tipe, ini dapat membantu mencegah properti berlebih.
interface Point {
x: number;
y: number;
}
const myPoint = { x: 10, y: 20, z: 30 };
// Buat tipe yang hanya menyertakan properti dari Point
const exactPoint: Point = myPoint as Omit & Point;
// Error: Tipe '{ x: number; y: number; z: number; }' tidak dapat ditetapkan ke tipe 'Point'.
// Literal objek hanya boleh menentukan properti yang dikenal, dan 'z' tidak ada dalam tipe 'Point'.
function printPoint(point: Point) {
console.log(`X: ${point.x}, Y: ${point.y}`);
}
//Perbaikan
const myPointCorrect = { x: 10, y: 20 };
const exactPointCorrect: Point = myPointCorrect as Omit & Point;
printPoint(exactPointCorrect);
Pendekatan ini akan memunculkan error jika `myPoint` memiliki properti yang tidak didefinisikan dalam antarmuka `Point`.
Penjelasan: `Omit
2. Menggunakan Fungsi untuk Membuat Objek
Anda dapat membuat fungsi pabrik (*factory function*) yang hanya menerima properti yang didefinisikan dalam antarmuka. Pendekatan ini memberikan pemeriksaan tipe yang kuat pada saat pembuatan objek.
interface Config {
apiUrl: string;
timeout: number;
}
function createConfig(config: Config): Config {
return {
apiUrl: config.apiUrl,
timeout: config.timeout,
};
}
const myConfig = createConfig({ apiUrl: "https://api.example.com", timeout: 5000 });
//Ini tidak akan dikompilasi:
//const myConfigError = createConfig({ apiUrl: "https://api.example.com", timeout: 5000, typo: true });
//Argumen tipe '{ apiUrl: string; timeout: number; typo: true; }' tidak dapat ditetapkan ke parameter tipe 'Config'.
// Literal objek hanya boleh menentukan properti yang dikenal, dan 'typo' tidak ada dalam tipe 'Config'.
Dengan mengembalikan objek yang dibangun hanya dengan properti yang didefinisikan dalam antarmuka `Config`, Anda memastikan bahwa tidak ada properti tambahan yang bisa menyelinap masuk. Ini membuat pembuatan konfigurasi menjadi lebih aman.
3. Menggunakan Penjaga Tipe (Type Guards)
Penjaga tipe adalah fungsi yang mempersempit tipe variabel dalam lingkup tertentu. Meskipun tidak secara langsung mencegah properti berlebih, mereka dapat membantu Anda secara eksplisit memeriksa keberadaannya dan mengambil tindakan yang sesuai.
interface User {
id: number;
name: string;
}
function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj && typeof obj.id === 'number' &&
'name' in obj && typeof obj.name === 'string' &&
Object.keys(obj).length === 2 //periksa jumlah kunci. Catatan: rapuh dan bergantung pada jumlah kunci persis dari User.
);
}
const potentialUser1 = { id: 123, name: "Alice" };
const potentialUser2 = { id: 456, name: "Bob", extra: true };
if (isUser(potentialUser1)) {
console.log("User Valid:", potentialUser1.name);
} else {
console.log("User Tidak Valid");
}
if (isUser(potentialUser2)) {
console.log("User Valid:", potentialUser2.name); //Tidak akan sampai di sini
} else {
console.log("User Tidak Valid");
}
Dalam contoh ini, penjaga tipe `isUser` tidak hanya memeriksa keberadaan properti yang diperlukan tetapi juga tipe dan jumlah *persis* propertinya. Pendekatan ini lebih eksplisit dan memungkinkan Anda menangani objek yang tidak valid dengan baik. Namun, pemeriksaan jumlah properti ini rapuh. Setiap kali `User` menambah/mengurangi properti, pemeriksaannya harus diperbarui.
4. Memanfaatkan `Readonly` dan `as const`
Meskipun `Readonly` mencegah modifikasi properti yang ada, dan `as const` membuat tupel atau objek hanya-baca di mana semua properti bersifat hanya-baca secara mendalam dan memiliki tipe literal, keduanya dapat digunakan untuk membuat definisi dan pemeriksaan tipe yang lebih ketat saat digabungkan dengan metode lain. Namun, keduanya tidak mencegah properti berlebih dengan sendirinya.
interface Options {
width: number;
height: number;
}
//Buat tipe Readonly
type ReadonlyOptions = Readonly;
const options: ReadonlyOptions = { width: 100, height: 200 };
//options.width = 300; //error: Tidak dapat menetapkan ke 'width' karena ini adalah properti hanya-baca.
//Menggunakan as const
const config = { api_url: "https://example.com", timeout: 3000 } as const;
//config.timeout = 5000; //error: Tidak dapat menetapkan ke 'timeout' karena ini adalah properti hanya-baca.
//Namun, properti berlebih masih diizinkan:
const invalidOptions: ReadonlyOptions = { width: 100, height: 200, depth: 300 }; //tidak ada error. Masih mengizinkan properti berlebih.
interface StrictOptions {
readonly width: number;
readonly height: number;
}
//Ini sekarang akan error:
//const invalidStrictOptions: StrictOptions = { width: 100, height: 200, depth: 300 };
//Tipe '{ width: number; height: number; depth: number; }' tidak dapat ditetapkan ke tipe 'StrictOptions'.
// Literal objek hanya boleh menentukan properti yang dikenal, dan 'depth' tidak ada dalam tipe 'StrictOptions'.
Ini meningkatkan imutabilitas, tetapi hanya mencegah mutasi, bukan keberadaan properti tambahan. Jika digabungkan dengan `Omit` atau pendekatan fungsi, ini menjadi lebih efektif.
5. Menggunakan Pustaka (misalnya, Zod, io-ts)
Pustaka seperti Zod dan io-ts menawarkan validasi tipe runtime dan kemampuan definisi skema yang kuat. Pustaka ini memungkinkan Anda mendefinisikan skema yang secara tepat menggambarkan bentuk data yang diharapkan, termasuk mencegah properti berlebih. Meskipun mereka menambahkan dependensi runtime, mereka menawarkan solusi yang sangat tangguh dan fleksibel.
Contoh dengan Zod:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
type User = z.infer;
const validUser = { id: 1, name: "John" };
const invalidUser = { id: 2, name: "Jane", extra: true };
const parsedValidUser = UserSchema.parse(validUser);
console.log("User Valid yang Diparsing:", parsedValidUser);
try {
const parsedInvalidUser = UserSchema.parse(invalidUser);
console.log("User Tidak Valid yang Diparsing:", parsedInvalidUser); // Ini tidak akan tercapai
} catch (error) {
console.error("Error Validasi:", error.errors);
}
Metode `parse` Zod akan memunculkan error jika input tidak sesuai dengan skema, yang secara efektif mencegah properti berlebih. Ini memberikan validasi runtime dan juga menghasilkan tipe TypeScript dari skema, memastikan konsistensi antara definisi tipe Anda dan logika validasi runtime.
Praktik Terbaik untuk Menegakkan Tipe Tepat
Berikut adalah beberapa praktik terbaik yang perlu dipertimbangkan saat menerapkan pencocokan bentuk objek yang lebih ketat di TypeScript:
- Pilih teknik yang tepat: Pendekatan terbaik tergantung pada kebutuhan spesifik dan persyaratan proyek Anda. Untuk kasus sederhana, penegasan tipe dengan `Omit` atau fungsi pabrik mungkin sudah cukup. Untuk skenario yang lebih kompleks atau ketika validasi runtime diperlukan, pertimbangkan untuk menggunakan pustaka seperti Zod atau io-ts.
- Jadilah konsisten: Terapkan pendekatan yang Anda pilih secara konsisten di seluruh basis kode Anda untuk menjaga tingkat keamanan tipe yang seragam.
- Dokumentasikan tipe Anda: Dokumentasikan antarmuka dan tipe Anda dengan jelas untuk mengkomunikasikan bentuk data yang diharapkan kepada pengembang lain.
- Uji kode Anda: Tulis tes unit untuk memverifikasi bahwa batasan tipe Anda berfungsi seperti yang diharapkan dan bahwa kode Anda menangani data yang tidak valid dengan baik.
- Pertimbangkan untung-ruginya: Menerapkan pencocokan bentuk objek yang lebih ketat dapat membuat kode Anda lebih tangguh, tetapi juga dapat meningkatkan waktu pengembangan. Timbang manfaatnya terhadap biayanya dan pilih pendekatan yang paling masuk akal untuk proyek Anda.
- Adopsi bertahap: Jika Anda bekerja pada basis kode besar yang sudah ada, pertimbangkan untuk mengadopsi teknik ini secara bertahap, dimulai dari bagian paling kritis dari aplikasi Anda.
- Lebih sukai antarmuka daripada alias tipe saat mendefinisikan bentuk objek: Antarmuka umumnya lebih disukai karena mendukung penggabungan deklarasi, yang dapat berguna untuk memperluas tipe di berbagai file.
Contoh Dunia Nyata
Mari kita lihat beberapa skenario dunia nyata di mana tipe tepat dapat bermanfaat:
- Payload permintaan API: Saat mengirim data ke API, sangat penting untuk memastikan bahwa payload sesuai dengan skema yang diharapkan. Menerapkan tipe tepat dapat mencegah kesalahan yang disebabkan oleh pengiriman properti yang tidak terduga. Misalnya, banyak API pemrosesan pembayaran sangat sensitif terhadap data yang tidak terduga.
- File konfigurasi: File konfigurasi sering kali berisi banyak properti, dan kesalahan ketik bisa sering terjadi. Menggunakan tipe tepat dapat membantu menangkap kesalahan ketik ini sejak dini. Jika Anda menyiapkan lokasi server dalam penerapan cloud, kesalahan ketik dalam pengaturan lokasi (misalnya eu-west-1 vs. eu-wet-1) akan menjadi sangat sulit untuk di-debug jika tidak terdeteksi di awal.
- Pipeline transformasi data: Saat mengubah data dari satu format ke format lain, penting untuk memastikan bahwa data keluaran sesuai dengan skema yang diharapkan.
- Antrean pesan: Saat mengirim pesan melalui antrean pesan, penting untuk memastikan bahwa payload pesan valid dan berisi properti yang benar.
Contoh: Konfigurasi Internasionalisasi (i18n)
Bayangkan mengelola terjemahan untuk aplikasi multi-bahasa. Anda mungkin memiliki objek konfigurasi seperti ini:
interface Translation {
greeting: string;
farewell: string;
}
interface I18nConfig {
locale: string;
translations: Translation;
}
const englishConfig: I18nConfig = {
locale: "en-US",
translations: {
greeting: "Hello",
farewell: "Goodbye"
}
};
//Ini akan menjadi masalah, karena ada properti berlebih, yang secara diam-diam memasukkan bug.
const spanishConfig: I18nConfig = {
locale: "es-ES",
translations: {
greeting: "Hola",
farewell: "Adiós",
typo: "unintentional translation"
}
};
//Solusi: Menggunakan Omit
const spanishConfigCorrect: I18nConfig = {
locale: "es-ES",
translations: {
greeting: "Hola",
farewell: "Adiós"
} as Omit & Translation
};
Tanpa tipe tepat, kesalahan ketik pada kunci terjemahan (seperti menambahkan bidang `typo`) bisa tidak terdeteksi, yang menyebabkan terjemahan hilang di antarmuka pengguna. Dengan menerapkan pencocokan bentuk objek yang lebih ketat, Anda dapat menangkap kesalahan ini selama pengembangan dan mencegahnya mencapai produksi.
Kesimpulan
Meskipun TypeScript tidak memiliki "tipe tepat" bawaan, Anda dapat mencapai hasil serupa menggunakan kombinasi fitur dan teknik TypeScript seperti penegasan tipe dengan `Omit`, fungsi pabrik, penjaga tipe, `Readonly`, `as const`, dan pustaka eksternal seperti Zod dan io-ts. Dengan menerapkan pencocokan bentuk objek yang lebih ketat, Anda dapat meningkatkan ketangguhan kode Anda, mencegah kesalahan umum, dan membuat aplikasi Anda lebih andal. Ingatlah untuk memilih pendekatan yang paling sesuai dengan kebutuhan Anda dan konsisten dalam menerapkannya di seluruh basis kode Anda. Dengan mempertimbangkan pendekatan-pendekatan ini secara cermat, Anda dapat mengambil kendali lebih besar atas tipe aplikasi Anda dan meningkatkan kemudahan pemeliharaan jangka panjang.